﻿// ---------------------------------------------------------------------------------------------------------------------
//   Copyright (c) SRT Solutions 2009. All rights reserved.
// ---------------------------------------------------------------------------------------------------------------------

using System;
using System.Linq;
using System.IO;

using NUnit.Framework;

using SRTSolutions.Elevate.IO;

using Moq;

namespace SRTSolutions.Elevate.Guide
{

    public class BufferedStreams
    {
        private class StreamForTesting : Stream
        {
            private Stream inputStream;
            public StreamForTesting(Stream inputStream)
            {
                this.inputStream = inputStream;
            }

            public int CanReadCount { get; private set; }

            public override bool CanRead
            {
                get { CanReadCount++; return inputStream.CanRead; }
            }

            public override bool CanSeek
            {
                get { return inputStream.CanSeek; }
            }

            public override bool CanWrite
            {
                get { return inputStream.CanWrite; }
            }

            public override void Flush()
            {
                inputStream.Flush();
            }

            public override long Length
            {
                get { return inputStream.Length; }
            }

            public override long Position
            {
                get { return inputStream.Position; }
                set { inputStream.Position = value; }
            }

            public int ReadCalledCount { get; private set; }

            public override int Read(byte[] buffer, int offset, int count)
            {
                ReadCalledCount++;
                return inputStream.Read(buffer, offset, count);
            }

            public override long Seek(long offset, SeekOrigin origin)
            {
                return inputStream.Seek(offset, origin);
            }

            public override void SetLength(long value)
            {
                inputStream.SetLength(value);
            }

            public override void Write(byte[] buffer, int offset, int count)
            {
                inputStream.Write(buffer, offset, count);
            }
        }

        private StreamForTesting GetStream()
        {
            return new StreamForTesting(new MemoryStream(0.Through(20).Select(x => (byte)x).ToArray()));
        }

        [Test]
        public void Constructing()
        {
            //given a stream
            using (var stream = GetStream())
            {
                //we can buffer it like this
                BufferedInputStream buffered = stream.Buffer(10);

                //the new stream will start off by reading in
                //the first bufferSize bytes of the input stream
                //into an in-memory buffer
                Assert.AreEqual(10, stream.Position);
            }
        }

        [Test]
        public void ReadingData()
        {
            //given a stream
            using (var stream = GetStream())
            {
                //once we buffer it
                var buffered = stream.Buffer(10);
                
                //if we read within the buffer size, we'll get the same
                //values that we would from reading the input stream
                Assert.AreEqual(0, buffered.ReadByte());
                Assert.AreEqual(1, buffered.ReadByte());
                Assert.AreEqual(2, buffered.ReadByte());
                Assert.AreEqual(3, buffered.ReadByte());
                Assert.AreEqual(4, buffered.ReadByte());

                //but we will only read once
                Assert.AreEqual(1, stream.ReadCalledCount);
            }
        }

        [Test]
        public void ReadingBeyondTheEndOfTheBuffer()
        {
            //given a stream that we buffered
            using (var Stream = GetStream())
            {
                var buffered = Stream.Buffer(5);
                
                //if we read beyond the end of the initial buffer
                var data = new byte[7];
                buffered.Read(data, 0, 7);

                //the data will be as expected
                Assert.AreEqual(0.Through(6).Select(x => (byte)x), data);

                //and we will have only needed to call read twice
                Assert.AreEqual(2, Stream.ReadCalledCount);
            }
        }

        [Test]
        public void ReadingEndOfStream()
        {
            using (var stream = GetStream())
            {
                var buffered = stream.Buffer(5);

                var data = 25.Through(50).Select(x=>(byte)x).ToArray();

                Assert.AreEqual(21, buffered.Read(data, 0, 25));

                var expected = Seq.Build(0.Through(20), 46.Through(50));
                Assert.AreEqual(expected, data);
                Assert.AreEqual(5, stream.ReadCalledCount);
            }
        }

        [Test]
        public void Repositioning()
        {
            //given a buffered stream
            using (var stream = GetStream())
            {
                var buffered = stream.Buffer(5);

                //if we set the position of the stream to be outside
                //the bounds of the buffer
                buffered.Position = 13;

                //the buffer will be set to that point
                Assert.AreEqual(2, stream.ReadCalledCount);

                Assert.AreEqual(13, buffered.ReadByte());
                Assert.AreEqual(14, buffered.ReadByte());
                Assert.AreEqual(15, buffered.ReadByte());

                Assert.AreEqual(2, stream.ReadCalledCount);
            }
        }

        [Test]
        public void RepositioningWithinStream()
        {
            //given a buffered stream
            using (var stream = GetStream())
            {
                var buffered = stream.Buffer(10);

                //if we set the position of the stream to be 
                //within the bounds of the buffer
                buffered.Position = 3;

                //the buffer will not be re-read
                Assert.AreEqual(1, stream.ReadCalledCount);

                Assert.AreEqual(3, buffered.ReadByte());
                Assert.AreEqual(4, buffered.ReadByte());
                Assert.AreEqual(5, buffered.ReadByte());
                Assert.AreEqual(6, buffered.ReadByte());

                Assert.AreEqual(1, stream.ReadCalledCount);
            }
        }

        [Test]
        public void RepositioningBehindBuffer()
        {
            using (var stream = GetStream())
            {
                var buffered = stream.Buffer(5);

                buffered.Position = 10;
                buffered.Position = 2;

                Assert.AreEqual(3, stream.ReadCalledCount);

                Assert.AreEqual(2, buffered.ReadByte());
                Assert.AreEqual(3, buffered.ReadByte());
                Assert.AreEqual(4, buffered.ReadByte());

                Assert.AreEqual(3, stream.ReadCalledCount);
            }
        }

        [Test]
        public void ThrowsWhenAnUnreadableStreamIsGiven()
        {
            var stream = new Mock<Stream>();

            stream.Setup(s => s.CanRead).Returns(false);

            Assert.Throws<ArgumentException>(() => stream.Object.Buffer(10));
        }

        private static Mock<Stream> GetMockedStream()
        {
            var stream = new Mock<Stream>();
            stream.Setup(s => s.CanRead).Returns(true);
            return stream;
        }

        [Test]
        public void CanWriteIsFalseAndNeverCallsInputStreamCanWrite()
        {
            var stream = new Mock<Stream>();
            stream.Setup(x => x.CanRead).Returns(true);

            var buffered = stream.Object.Buffer(10);

            stream.Setup(x => x.CanRead).Returns(true);
            stream.Setup(s => s.CanWrite).Returns(true);
            stream.VerifyGet(s => s.CanWrite, Times.Never());

            Assert.IsFalse(buffered.CanWrite);
        }

        [Test]
        public void WriteThrowsAndNeverCallsInputStreamWrite()
        {
            var mocked = new Mock<Stream>();
            mocked.Setup(x => x.CanRead).Returns(true);

            var buffered = mocked.Object.Buffer(10);

            mocked.VerifyGet(s => s.CanWrite, Times.Never());
            mocked.Verify(s => s.Write(It.IsAny<byte[]>(), It.IsAny<int>(), It.IsAny<int>()), Times.Never());
            mocked.VerifyGet(stream => stream.CanWrite, Times.Never());

            var copyTo = new byte[10];
            Assert.Throws<NotSupportedException>(() => buffered.Write(copyTo, 0, 10));
        }

        [Test]
        public void CanSeekIsDeterminedFromInputStreamFalse()
        {
            var mocked = GetMockedStream();
            var buffered = mocked.Object.Buffer(10);

            mocked.Setup(s => s.CanSeek).Returns(false);

            Assert.IsFalse(buffered.CanSeek);

            mocked.VerifyGet(s => s.CanSeek, Times.AtLeastOnce());
        }

        [Test]
        public void SetLengthWillThrowEvenOnWriteAbleInputStreams()
        {
            var mocked = GetMockedStream();
            mocked.Setup(stream => stream.CanWrite).Returns(true);

            var buffered = mocked.Object.Buffer(10);

            Assert.Throws<NotSupportedException>(() => buffered.SetLength(100));

            mocked.Verify(stream => stream.Write(It.IsAny<byte[]>(), It.IsAny<int>(), It.IsAny<int>()), Times.Never());
            mocked.VerifyGet(stream => stream.CanWrite, Times.Never());
        }

        [Test]
        public void CanSeekIsDeterminedFromInputStreamTrue()
        {
            var mocked = GetMockedStream();
            var buffered = mocked.Object.Buffer(10);

            mocked.Setup(s => s.CanSeek).Returns(true);

            Assert.IsTrue(buffered.CanSeek);

            mocked.VerifyGet(s => s.CanSeek, Times.AtLeastOnce());
        }

        [Test]
        public void SeekingFromTheBeginningWithinTheBufferDoesntSeekInBaseStream()
        {
            var mocked = GetMockedStream();

            mocked.Setup(s => s.CanSeek).Returns(true);
            mocked.Setup(s => s.Length).Returns(20);

            var seekCount = 0;
            mocked.SetupSet(stream => stream.Position).Callback(ignored => seekCount++);

            var readCount = 0;
            mocked.Setup(stream => stream.Read(It.IsAny<byte[]>(), It.IsAny<int>(), It.IsAny<int>())).Callback(() => readCount++);

            var buffered = mocked.Object.Buffer(10);

            var beforeSeekCount = seekCount;
            var beforeReadCount = readCount;

            buffered.Seek(5, SeekOrigin.Begin);

            Assert.AreEqual(buffered.Position, 5);
            Assert.AreEqual(beforeSeekCount, seekCount);
            Assert.AreEqual(beforeReadCount, readCount);
        }

        [Test]
        public void SeekingFromTheBeginningOutsideTheBufferSeeksBaseStream()
        {
            var mocked = GetMockedStream();

            mocked.Setup(s => s.CanSeek).Returns(true);
            mocked.Setup(s => s.Length).Returns(20);

            var seekCount = 0;
            mocked.SetupSet(stream => stream.Position).Callback(ignored => seekCount++);

            var readCount = 0;
            mocked.Setup(stream => stream.Read(It.IsAny<byte[]>(), It.IsAny<int>(), It.IsAny<int>())).Callback(() => readCount++);

            var buffered = mocked.Object.Buffer(2);

            var beforeSeekCount = seekCount;
            var beforeReadCount = readCount;

            buffered.Seek(10, SeekOrigin.Begin);

            Assert.AreEqual(buffered.Position, 10);
            Assert.AreEqual(beforeSeekCount + 1, seekCount);
            Assert.AreEqual(beforeReadCount + 1, readCount);
        }

        [Test]
        public void SeekingFromCurrentWithinTheBufferDoesntSeekInBaseStream()
        {
            var mocked = GetMockedStream();

            mocked.Setup(s => s.CanSeek).Returns(true);
            mocked.Setup(s => s.Length).Returns(20);

            var seekCount = 0;
            mocked.SetupSet(stream => stream.Position).Callback(ignored => seekCount++);

            var readCount = 0;
            mocked.Setup(stream => stream.Read(It.IsAny<byte[]>(), It.IsAny<int>(), It.IsAny<int>())).Callback(() => readCount++);

            var buffered = mocked.Object.Buffer(10);

            var beforeSeekCount = seekCount;
            var beforeReadCount = readCount;

            buffered.Seek(5, SeekOrigin.Current);

            Assert.AreEqual(buffered.Position, 5);
            Assert.AreEqual(beforeSeekCount, seekCount);
            Assert.AreEqual(beforeReadCount, readCount);
        }

        [Test]
        public void SeekingFromTheCurrentOutsideTheBufferSeeksBaseStream()
        {
            var mocked = GetMockedStream();

            mocked.Setup(s => s.CanSeek).Returns(true);
            mocked.Setup(s => s.Length).Returns(20);

            var seekCount = 0;
            mocked.SetupSet(stream => stream.Position).Callback(ignored => seekCount++);

            var readCount = 0;
            mocked.Setup(stream => stream.Read(It.IsAny<byte[]>(), It.IsAny<int>(), It.IsAny<int>())).Callback(() => readCount++);

            var buffered = mocked.Object.Buffer(2);

            var beforeSeekCount = seekCount;
            var beforeReadCount = readCount;

            buffered.Seek(10, SeekOrigin.Current);

            Assert.AreEqual(buffered.Position, 10);
            Assert.AreEqual(beforeSeekCount + 1, seekCount);
            Assert.AreEqual(beforeReadCount + 1, readCount);
        }

        [Test]
        public void SeekingFromEndWithinTheBufferDoesntSeekInBaseStream()
        {
            var mocked = GetMockedStream();

            mocked.Setup(s => s.CanSeek).Returns(true);
            mocked.SetupGet(s => s.Length).Returns(10);

            var seekCount = 0;
            mocked.SetupSet(stream => stream.Position).Callback(ignored => seekCount++);

            var readCount = 0;
            mocked.Setup(stream => stream.Read(It.IsAny<byte[]>(), It.IsAny<int>(), It.IsAny<int>())).Callback(() => readCount++);

            var buffered = mocked.Object.Buffer(5);

            var beforeSeekCount = seekCount;
            var beforeReadCount = readCount;

            buffered.Seek(-7, SeekOrigin.End);

            Assert.AreEqual(buffered.Position, 3);
            Assert.AreEqual(beforeSeekCount, seekCount);
            Assert.AreEqual(beforeReadCount, readCount);
        }

        [Test]
        public void SeekingFromEndOutsideTheBufferRepositionsBaseStream()
        {
            var mocked = GetMockedStream();

            mocked.Setup(s => s.CanSeek).Returns(true);
            mocked.SetupGet(s => s.Length).Returns(10);

            var seekCount = 0;
            mocked.SetupSet(stream => stream.Position).Callback(ignored => seekCount++);

            var readCount = 0;
            mocked.Setup(stream => stream.Read(It.IsAny<byte[]>(), It.IsAny<int>(), It.IsAny<int>())).Callback(() => readCount++);

            var buffered = mocked.Object.Buffer(2);

            var beforeSeekCount = seekCount;
            var beforeReadCount = readCount;

            buffered.Seek(-3, SeekOrigin.End);

            Assert.AreEqual(buffered.Position, 7);
            Assert.AreEqual(beforeSeekCount + 1, seekCount);
            Assert.AreEqual(beforeReadCount + 1, readCount);
        }
    }
}
